﻿using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
using Microsoft.Practices.Unity.Utility;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;

namespace Artech.Core.Caching
{
    /// <summary>
    /// The Entib Caching based cache store.
    /// </summary>
    /// <typeparam name="T">The type of cached data.</typeparam>
    public class DefaultCacheStore<T>: ICacheStore<T>
    {
        private static CacheKeys cacheKeys = new CacheKeys();
        private object syncHelper = new object();

        /// <summary>
        /// Gets the expiration mode.
        /// </summary>
        /// <value>
        /// The expiration mode.
        /// </value>
        public CacheExpirationMode ExpirationMode { get; private set; }

        /// <summary>
        /// Gets the expired time expression.
        /// </summary>
        /// <value>
        /// The expired time expression.
        /// </value>
        public string ExpiredTime { get; private set; }

        /// <summary>
        /// Gets the name of the cache depedency file.
        /// </summary>
        /// <value>
        /// The name of the cache depedency file.
        /// </value>
        public string DepedencyFileName { get; private set; }

        /// <summary>
        /// Gets the prefix of the key of cache item.
        /// </summary>
        /// <value>
        /// The prefix of the key of cache item.
        /// </value>
        public string Prefix { get;private set; }

        /// <summary>
        /// Gets the cache manager.
        /// </summary>
        /// <value>
        /// The cache manager.
        /// </value>
        public ICacheManager CacheManager { get; private set; }

        /// <summary>
        /// Gets the cache item priority.
        /// </summary>
        /// <value>
        /// The cache item priority.
        /// </value>
        public CacheItemPriority CacheItemPriority { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultCacheStore{T}"/> class.
        /// </summary>
        /// <param name="prefix">The prefix.</param>
        /// <param name="cacheManagerName">Name of the cache manager.</param>
        /// <param name="cacheItemPriority">The cache item priority.</param>
        /// <param name="expirationMode">The expiration mode.</param>
        /// <param name="expirationTime">The expiration time.</param>
        /// <param name="fileName">Name of the file.</param>
        public DefaultCacheStore(string prefix, string cacheManagerName, CacheItemPriority cacheItemPriority, CacheExpirationMode expirationMode, string expirationTime, string fileName)
        {
            Guard.ArgumentNotNullOrEmpty(prefix, "prefix");
            if (expirationMode == CacheExpirationMode.FileDepedency)
            {
                Guard.ArgumentNotNullOrEmpty(fileName, "fileName");
            }
            if (expirationMode == CacheExpirationMode.Absolute || expirationMode == CacheExpirationMode.Sliding || expirationMode == CacheExpirationMode.TimeFormat)
            {
                Guard.ArgumentNotNullOrEmpty(expirationTime, "expirationTime");
            }
            if (string.IsNullOrEmpty(cacheManagerName))
            {
                this.CacheManager = CacheFactory.GetCacheManager();
            }
            else
            {
                this.CacheManager = CacheFactory.GetCacheManager(cacheManagerName);
            }
            this.Prefix = prefix;
            this.ExpirationMode = expirationMode;
            this.ExpiredTime = expirationTime;
            this.DepedencyFileName = fileName;
            this.CacheItemPriority = cacheItemPriority;
        }

        /// <summary>
        /// Gets the data.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="getData">The get data.</param>
        /// <returns></returns>
        public T GetData(string key, Func<T> getData = null)
        {
            Guard.ArgumentNotNullOrEmpty(key, "key");
            key = string.Format("{0}.{1}", this.Prefix, key);
            object value = this.CacheManager.GetData(key);
            if (null == value)
            {
                if (null != getData)
                {
                    T data = getData();
                    this.AddData(key, data);
                    return data;
                }
                return default(T);
            }
            return (T)value;
        }
        /// <summary>
        /// Saves the data.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="data">The data.</param>
        public void AddData(string key, T data)
        {
            Guard.ArgumentNotNullOrEmpty(key, "key");
            key = string.Format("{0}.{1}", this.Prefix, key);
            lock (syncHelper)
            {
                this.CacheManager.Add(key, data, this.CacheItemPriority, new DefaultCacheItemRefreshAction(), this.CreateCacheItemExpiration());
                cacheKeys.Add(this.Prefix, key);
            }
        }
        /// <summary>
        /// Clears this instance.
        /// </summary>
        public void Clear()
        {
            List<string> keys = cacheKeys.GetKeys(this.Prefix);
            foreach (string key in keys)
            {
                this.CacheManager.Remove(key);
            }
        }

        private ICacheItemExpiration CreateCacheItemExpiration()
        {
            switch (this.ExpirationMode)
            {
                case CacheExpirationMode.Absolute: return new AbsoluteTime(TimeSpan.Parse(this.ExpiredTime));
                case CacheExpirationMode.Sliding: return new SlidingTime(TimeSpan.Parse(this.ExpiredTime));
                case CacheExpirationMode.TimeFormat: return new ExtendedFormatTime(this.ExpiredTime);
                case CacheExpirationMode.FileDepedency: return new FileDependency(this.DepedencyFileName);
                default: return new NeverExpired();
            }
        }
        private class DefaultCacheItemRefreshAction : ICacheItemRefreshAction
        {
            public void Refresh(string removedKey, object expiredValue, CacheItemRemovedReason removalReason)
            {
                string prefix = cacheKeys.GetPrefix(removedKey);
                if (!string.IsNullOrEmpty(prefix))
                {
                    cacheKeys.Remove(prefix, removedKey);
                }
            }
        }
        private class CacheKeys
        {
            private Dictionary<string, List<string>> prefixKeys = new Dictionary<string, List<string>>();
            private Dictionary<string, string> keyPrefixes = new Dictionary<string, string>();
            private object syncHelper = new object();

            public void Add(string prefix, string key)
            {
                List<string> keys;
                if (prefixKeys.TryGetValue(prefix, out keys))
                {
                    if (!keys.Contains(key))
                    {
                        lock (syncHelper)
                        {
                            if (!keys.Contains(key))
                            {
                                keys.Add(key);
                                keyPrefixes.Add(key, prefix);
                            }
                        }
                    }
                }
                else
                {
                    keys = new List<string> { key };
                    lock (syncHelper)
                    {
                        if(!prefixKeys.ContainsKey(prefix))
                        {
                            prefixKeys.Add(prefix, keys);
                            keyPrefixes.Add(key, prefix);
                        }
                    }
                }
            }
            public void Remove(string prefix)
            {
                List<string> keys;
                if (!prefixKeys.TryGetValue(prefix, out keys))
                {
                    return;
                }
                lock (syncHelper)
                {
                    if (!prefixKeys.TryGetValue(prefix, out keys))
                    {
                        return;
                    }
                    foreach (string key in keys)
                    {
                        prefixKeys.Remove(key);
                    }
                    prefixKeys.Remove(prefix);
                }
            }
            public void Remove(string prefix, string key)
            {
                List<string> keys;
                if(!prefixKeys.TryGetValue(prefix, out keys))
                {
                    return;
                }
                if(!keys.Contains(key))
                {
                    return;
                }
                lock (syncHelper)
                {
                    if (!prefixKeys.TryGetValue(prefix, out keys))
                    {
                        return;
                    }
                    if (!keys.Contains(key))
                    {
                        return;
                    }
                    keys.Remove(key);
                    prefixKeys.Remove(key);
                }
            }

            public List<string> GetKeys(string prefix)
            {
                List<string> keys;
                if (prefixKeys.TryGetValue(prefix, out keys))
                {
                    return keys;
                }
                return new List<string>();
            }
            public string GetPrefix(string key)
            {
                string prefix;
                if (keyPrefixes.TryGetValue(key, out prefix))
                {
                    return prefix;
                }
                return null;
            }
        }
    }
}